#[macro_use]
extern crate clap;
extern crate md5;
extern crate regex;
extern crate tempfile;
extern crate threadpool;
extern crate walkdir;
#[macro_use]
extern crate lazy_static;

extern crate ftp;
extern crate ssh2;

use std::collections::HashMap;
use std::error::Error;
use std::fs;
use std::fs::metadata;
use std::io::{Read, Write};
use std::path::Path;
use std::process::exit;
use std::sync::mpsc::channel;
use std::sync::mpsc::{Receiver, Sender};
use std::sync::Mutex;
// use std::io::Read;

// use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
// use std::path::Path;
use ssh2::Session;

use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};
use md5::compute;
use threadpool::ThreadPool;
use walkdir::WalkDir;
// use tempfile::tempfile;
use tempfile::NamedTempFile;
// use log::info;
use ftp::FtpStream;
use std::io;
use std::io::copy;
use zip::write::FileOptions;
use zip::ZipWriter;

use chrono::DateTime;
use chrono::Utc;
use std::time::SystemTime;

const HELP: &str = r#"
    search grep by -f 
    upload file like :
        -a sftp://localhost:22@name:pwd
        -a ftp://localhost:22@name:pwd
        -a tcp://localhost:222
    listen a tcp file:
        -a listen://0.0.0.0:12345@/tmp/some.zip
"#;

fn tcp_listen_to_file(f_name: &str, local_addr: &str) -> E {
    let lis = TcpListener::bind(local_addr).expect("bind error ");
    let pb = ProgressBar::new(100000000);
    pb.set_style(ProgressStyle::default_bar()
            .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} {bytes_per_sec} ({eta})")
            .progress_chars("#>-"));
    for stream in lis.incoming() {
        let mut f = fs::File::create(f_name)?;
        let mut reader = stream.expect("no readable handler");
        copy_with_progress(&mut reader, &mut f, &pb).unwrap();
        pb.finish();
        break;
    }
    Ok(())
}

fn copy_with_progress<R: ?Sized, W: ?Sized>(
    reader: &mut R,
    writer: &mut W,
    pro: &ProgressBar,
) -> io::Result<u64>
where
    R: Read,
    W: Write,
{
    let mut buf = [0; 34096];
    let mut written = 0;
    loop {
        // pro.println(&format!("read wait "));
        let len = match reader.read(&mut buf) {
            Ok(0) => return Ok(written),
            Ok(len) => len,
            // Err(ref e ) if e.kind() == io::ErrorKind::Interrupted => continue,
            Err(e) => {
                pro.println(&format!("{}", e));
                return Err(e);
            }
        };

        // pro.println(&format!("read {}", written));
        writer.write(&buf[..len]).expect("write error ");
        written += len as u64;
        pro.inc(len as u64);
        buf = [0; 34096];
    }
}

struct Account {
    tp: String,
    name: String,
    pwd: String,
    host: String,
    save_file: String,
}

impl Default for Account {
    fn default() -> Self {
        Account {
            tp: "".to_string(),
            name: "anonymous".to_string(),
            pwd: "anonymous".to_string(),
            host: "0.0.0.0:10022".to_string(),
            save_file: "".to_string(),
        }
    }
}
impl std::fmt::Display for Account {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}://{}@{}:{}",
            &self.tp, &self.host, &self.name, &self.pwd
        )
    }
}
fn now() -> String {
    let sys_time = SystemTime::now();
    let datetime: DateTime<Utc> = sys_time.into();
    format!("{}", datetime.format("%Y-%m-%T"))
}

impl Account {
    fn upload_ftp(&self) -> E {
        let mut ftp_stream = FtpStream::connect(&self.host).expect("connect ftp failed");
        println!("conected [{}]", "ok".green());

        let _ = ftp_stream
            .login(&self.name, &self.pwd)
            .expect("login failed");
        println!("authenticate [{}]", "ok".green());

        let mut reader = TEMP_FILE.reopen().expect("reopen failed!");
        let _ = ftp_stream.put(&self.get_remote_name(), &mut reader);
        println!("upload to ftp://{} [{}]", &self.host.yellow(), "ok".green());
        Ok(())
    }
    fn upload_tcp(&self) -> E {
        let pb = ProgressBar::new(self.get_size());
        pb.set_style(ProgressStyle::default_bar()
            .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} {bytes_per_sec} ({eta})")
            .progress_chars("#>-"));
        let mut tcp = TcpStream::connect(&self.host).expect("connect ssh failed");
        let mut from_file = TEMP_FILE.reopen().expect("reopen failed");

        copy_with_progress(&mut from_file, &mut tcp, &pb).expect("send tcp failed");
        Ok(())
    }

    fn listen(&self) {
        let _ = tcp_listen_to_file(&self.save_file, &self.host);
    }

    fn get_remote_name(&self) -> String {
        format!("/tmp/{}.zip", &now())
    }
    fn get_size(&self) -> u64 {
        let path = TEMP_FILE.path().to_str().unwrap();
        let m1 = metadata(path).expect("no meta");
        m1.len() as u64
    }

    fn upload_sftp(&self) -> E {
        let pb = ProgressBar::new(self.get_size());
        pb.set_style(ProgressStyle::default_bar()
            .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} {bytes_per_sec} ({eta})")
            .progress_chars("#>-"));
        let tcp = TcpStream::connect(&self.host).expect("connect ssh failed");
        println!("conected [{}]", "ok".green());

        let mut sess = Session::new()?;
        sess.set_tcp_stream(tcp);
        sess.handshake()?;
        println!("authenticate ready [{}]", "ok".green());

        sess.userauth_password(&self.name, &self.pwd)?;
        println!("authenticate [{}]", "ok".green());

        let sftp = sess.sftp().expect("can not use sftp ?");
        let mut remote_file = sftp
            .create(Path::new(&self.get_remote_name()))
            .expect("sftp create error!");
        println!(
            "create sftp file: {} [{}]",
            &self.get_remote_name().yellow(),
            "ok".green()
        );

        // let mut remote_file = sess.scp_send(Path::new(&self.get_remote_name()),
        //                                     0o644, 10, None)?;
        let mut from_file = TEMP_FILE.reopen().expect("reopen failed");
        // copy(&mut from_file, &mut remote_file).expect("upload failed");
        copy_with_progress(&mut from_file, &mut remote_file, &pb).expect("upload failed");
        println!("upload [{}]", "ok".green());

        Ok(())
    }

    fn change_account(&mut self, acc: &str) {
        if acc.contains("://") {
            let head: Vec<&str> = acc.split("://").collect();
            let tail: Vec<&str> = head.last().unwrap().split("@").collect();

            self.tp = head.first().unwrap().to_string();
            self.host = tail.first().unwrap().to_string();
            if !self.host.contains(":") {
                self.host = format!("{}:22", &self.host);
            }
            if acc.contains("@") {
                if tail.last().unwrap().contains(":") {
                    let token: Vec<&str> = tail.last().unwrap().split(":").collect();
                    self.name = token.first().unwrap().to_string();
                    self.pwd = token.last().unwrap().to_string();
                } else {
                    self.save_file = tail.last().unwrap().to_string();
                }
            }
        }
    }

    fn upload(&self) {
        match self.tp.as_str() {
            "ftp" => {
                self.upload_ftp().expect("upload faild");
            }
            "sftp" => {
                self.upload_sftp().expect("upload failed");
            }
            "tcp" => {
                self.upload_tcp().expect("upload failed");
            }
            "listen" => {
                self.listen();
            }
            _ => {
                println!("help \n{}", HELP.green());
            }
        };
    }
}

lazy_static! {
    static ref TEMP_FILE: NamedTempFile =
        { NamedTempFile::new().expect("can not build temp file") };
    static ref TEMP_ZIP_FILE: Mutex<ZipWriter<fs::File>> = {
        let f = TEMP_FILE.reopen().expect("reopen temp file error");
        Mutex::new(ZipWriter::new(f))
    };
    static ref AFTER_TO_DO: Mutex<Account> = { Mutex::new(Account::default()) };
}

type R<T> = Receiver<T>;
type S<T> = Sender<T>;
type E = Result<(), Box<dyn Error>>;

const SEP: &str = "_:.:_";

fn file_md5(f_name: &str) -> Option<String> {
    let f = Path::new(f_name);
    let mut buf: Vec<u8> = vec![];
    if f.exists() && f.is_file() {
        let mut ff = fs::File::open(f).expect("file not exists this moment!");
        let _ = ff.read_to_end(&mut buf).expect("read bytes error");
        let digest = compute(buf);
        // let fname = f.file_name().expect("no filename").to_str().expect("no str");
        Some(format!("{:?}{}{}", digest, SEP, f_name))
    } else {
        None
    }
}

fn zip_file(f_paths: &str, pb: &ProgressBar) -> E {
    let path = Path::new(f_paths);
    if !path.exists() {
        return Ok(());
    }
    let dir_path = path.parent().unwrap();
    if !dir_path.exists() {
        return Ok(());
    }

    let mut zip = TEMP_ZIP_FILE.lock().expect("unlock zip file failed");

    zip.add_directory_from_path(dir_path, Default::default())?;

    let options = FileOptions::default()
        .compression_method(zip::CompressionMethod::Stored)
        .unix_permissions(0o755);
    zip.start_file_from_path(path, options)?;
    let mut reader = fs::File::open(path)?;
    let write_num = match copy(&mut reader, &mut *zip) {
        Ok(i) => i,
        Err(e) => {
            pb.println(&format!(
                "zip error file: {}, {}",
                path.to_str().unwrap(),
                e
            ));
            0
        }
    };

    pb.println(&format!(
        "pack file: {} : {:.5}M",
        path.file_name().unwrap().to_str().unwrap(),
        write_num as f64 / 1024.0 / 1024.0
    ));
    Ok(())
}

// fn base_name(f_name:&str) -> String{
//     Path::new(f_name).file_name().expect("no filename").to_str().expect("no str").to_string()
// }

fn size(f_name: &str) -> f64 {
    let m1 = metadata(f_name).expect("no meta");
    (m1.len() as f64) / 1024.0 / 1024.0
}

fn f_size(f_l: &str) -> String {
    let f: Vec<&str> = f_l.split(SEP).collect();
    let fsize = size(f[1]);
    format!("{} {} {}\n", f[0], &fsize, f[1])
}

fn write_dup_file(f_name: &str, m: &Vec<String>) -> E {
    let mut out_f = fs::File::create(f_name)?;
    let mut buf: String = String::from("");
    for mf in m {
        if let Some(line) = file_md5(mf) {
            buf.push_str(&f_size(&line));
        };
    }
    out_f.write_all(buf.as_bytes()).expect("write error");
    Ok(())
}

fn compare_modify(f1: &str, f2: &str) -> (String, String) {
    let m1 = match metadata(f1) {
        Ok(i) => i.modified().expect("no modify"),
        Err(_) => {
            return (f1.to_string(), f2.to_string());
        }
    };
    let m2 = match metadata(f2) {
        Ok(i) => i.modified().expect("no modify"),
        Err(_) => {
            return (f1.to_string(), f2.to_string());
        }
    };

    if m1 < m2 {
        (f1.to_string(), f2.to_string())
    } else {
        (f2.to_string(), f1.to_string())
    }
}

fn watcher(rx: R<String>, to_file_path: &str) {
    let mut md5_map: HashMap<String, String> = HashMap::new();
    let mut more_than_one: Vec<String> = vec![];
    let mut pb = ProgressBar::new(200000);
    pb.set_style(
        ProgressStyle::default_bar()
            .template(
                "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/files ({eta})",
            )
            .progress_chars("#>-"),
    );
    let mut count: u64 = 0;
    let mut all_count: i128 = -1;
    let mut all_size: f64 = 0.0;
    // let mut no_need_remove: Vec<String> = vec![];
    loop {
        let f_md5 = match rx.recv() {
            Ok(e) => e,
            Err(_) => {
                pb.println(&format!("{}", "End ".yellow()));
                pb.finish();
                break;
            }
        };
        if f_md5.starts_with("[") && f_md5.ends_with("]") {
            unsafe {
                all_count = f_md5
                    .get_unchecked(1..f_md5.len() - 1)
                    .to_string()
                    .parse::<i128>()
                    .expect("err");
            }

            pb = ProgressBar::new(all_count as u64);
            pb.set_style(ProgressStyle::default_bar()
                .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/files ({eta})")
                .progress_chars("#>-"));
            continue;
        }

        if count == all_count as u64 {
            pb.println(&format!("{}", "End ".yellow()));
            break;
        }
        if !&f_md5.contains(SEP) {
            pb.println(&format!("{} -> {}", "End ilige".yellow(), &f_md5.red()));
            break;
        }
        let fields: Vec<&str> = f_md5.split(SEP).collect();
        count += 1;

        if md5_map.contains_key(fields[0]) {
            let sim = md5_map[fields[0]].clone();
            let (oldf, newf) = compare_modify(&sim, fields[1]);
            if more_than_one.contains(&oldf) {
                // no_need_remove.push(oldf.clone());
                let mut remove_ix: i64 = -1;
                for i in 0..(more_than_one.len() - 1) {
                    if &more_than_one[i] == &oldf {
                        remove_ix = i as i64;
                        break;
                    }
                }
                if remove_ix != -1 {
                    more_than_one.remove(remove_ix as usize);
                }
                md5_map.insert(fields[0].to_string(), oldf);
            }
            more_than_one.push(newf.to_string());

            // let fr = base_name(fields[1]).replace("\n", " ");
            // let to = base_name(&sim).replace("\n"," ");
            // let uid = &fields[0][0..3];
            // let fsize_s = format!("{:.5}m", fsize);
            // pb.println(format!("md5:{}|{}| {} copy from {}", uid.green(),fsize_s.blue(), fr.green(), to.red()));

            let fsize = size(fields[1]);
            all_size += fsize;
        } else {
            md5_map.insert(fields[0].to_string(), fields[1].to_string());
            let _ = zip_file(fields[1], &pb);
        }
        pb.set_position(count);
    }
    if to_file_path == "[None]" {
        let fsize_s = format!("{:.5}m", all_size);
        println!(
            " ---------------- to file ------------- \n alll : {}",
            fsize_s.red()
        );
    } else {
        println!(
            "{}\n{}",
            " ---------------- to file ------------- ".green(),
            to_file_path.green()
        );
        let _ = write_dup_file(to_file_path, &more_than_one);
    }
    let mut zip_handle = TEMP_ZIP_FILE.lock().unwrap();
    zip_handle.finish().expect("finish zip failed");
    let zip_file = &TEMP_FILE;
    let f_name = (*zip_file).path().to_str().unwrap();

    println!(
        "packed file in : {} | size: {:.5}m",
        f_name.yellow(),
        size(f_name)
    );
}

fn walk_md5(
    pool: &ThreadPool,
    tx: S<String>,
    dirs: &Vec<String>,
    size_fileter: u64,
    file_filter: &str,
) {
    let mut all_count: i64 = 0;
    'all_loop: for path_dir_str in dirs {
        let path_dir = Path::new(path_dir_str);

        println!("scan in : {}", path_dir.display());

        for entry in WalkDir::new(&path_dir) {
            let entry = match entry {
                Ok(e) => e,
                _ => continue,
            };

            if entry.file_type().is_dir() {
                continue;
            }
            let f_tp = entry
                .path()
                .file_name()
                .unwrap()
                .to_str()
                .unwrap()
                .split(".")
                .last()
                .unwrap();

            if file_filter != "" {
                if !file_filter.contains(f_tp) {
                    continue;
                }

                let filters: Vec<&str> = file_filter.split(",").collect();
                let mut found = false;
                'test: for ptp in filters {
                    if f_tp == ptp {
                        found = true;
                        break 'test;
                    }
                }

                if !found {
                    continue;
                }
            }

            let f_path = entry.path();
            match metadata(f_path) {
                Ok(e) => {
                    if e.len() < size_fileter {
                        continue;
                    }
                }
                _ => continue,
            };

            let tx_thread = tx.clone();
            let f_path_string = f_path.to_str().expect("path to str error").to_string();
            pool.execute(move || {
                if let Some(out) = file_md5(&f_path_string) {
                    match tx_thread.send(out) {
                        Ok(_) => {}
                        Err(e) => {
                            println!("{}", e);
                            exit(0x0100);
                        }
                    }
                };
            });
            all_count += 1;
        }
    }
    tx.send(format!("[{}]", all_count)).expect("send failed");
}

#[derive(Clap)]
#[clap(
    version = "1.1",
    author = "Qingluan",
    about = r#"search grep by -f 
    upload file like :
        -a sftp://localhost:22@name:pwd
        -a ftp://localhost:22@name:pwd
        -a tcp://localhost:222
    listen a tcp file:
        -a listen://0.0.0.0:12345@/tmp/some.zip"#
)]
struct Opts {
    #[clap(short = "r", long = "root", multiple = true)]
    roots: Vec<String>,

    #[clap(short = "g", long = "grep", multiple = true)]
    filter_tp: Vec<String>,

    #[clap(short = "o", long = "output", default_value = "[None]")]
    dst: String,

    #[clap(short = "a", long = "account", default_value = "[None]")]
    account: String,

    // #[clap(long="clean")]
    // clear_db:bool,
    #[clap(short = "f", long = "filter_size", default_value = "4")]
    size_fileter: u64,

    #[clap(short = "t", long = "thread", default_value = "3")]
    thread: usize,
    // #[clap(short="l",long = "listen", default_value = "[None]")]
    // recv_to_file: String,
}

fn main() {
    let (tx, rx) = channel::<String>();
    let opts = Opts::parse();

    let mut tps: Vec<String> = opts.filter_tp.clone();
    if tps.len() == 0 {
        println!("grep tp :{}, use default: docx,xlsx,db,pdf,sql ", tps.len());
        tps.push("docx".to_string());
        tps.push("xlsx".to_string());
        tps.push("db".to_string());
        tps.push("pdf".to_string());
        tps.push("sql".to_string());
    // tps.push(".")
    } else {
        println!("tps: {:?}", &tps);
    }
    if opts.roots.len() > 0 {
        let pool = ThreadPool::with_name("worker".into(), opts.thread);
        let dst: String;

        dst = opts.dst.clone();
        pool.execute(move || {
            watcher(rx, &dst);
        });
        walk_md5(&pool, tx, &opts.roots, opts.size_fileter, &tps.join(","));
        pool.join()
    }

    if opts.account != "[None]" {
        let mut acc = AFTER_TO_DO.lock().expect("unlock account failed");
        acc.change_account(&opts.account);
        println!("use account : {} -> {}", acc, &acc.save_file.red());
        acc.upload();
    }

    // if opts.recv_to_file != "[None]"{
    //     let acc = AFTER_TO_DO.lock().expect("unlock account failed");
    //     let _ =  acc.upload();
    //     exit(0);
    // }
}
